/*
 *	CODESPACE
 *	A real-time syntax highlighting javascript editor
 *
 *	Copyright (C) 2008 Francois Botman, <Franchie.brightspark@gmail.com>
 *
 *	This script is licenced under: Creative Commons Attribution-Noncommercial-Share Alike 3.0
 *	(see http://creativecommons.org/licenses/by-nc-sa/3.0/).
 *	It bascially means that it can be modified and shared, provided it is
 *	clearly attributed, and can be used in personnal/charity projects for free!
 *	(Commercial use requires prior negotiation with the author).
 *	In any case, should anything go wrong, I cannot be held responsible!
 *
 *	-----------
 */


// Compatibility for legacy browsers (IE):
if(!window.Node) Node = {
	ELEMENT_NODE: 1,
	ATTRIBUTE_NODE: 2,
	TEXT_NODE: 3,
	CDATA_SECTION_NODE: 4,
	ENTITY_REFERENCE_NODE: 5,
	ENTITY_NODE: 6,
	PROCESSING_INSTRUCTION_NODE: 7,
	COMMENT_NODE: 8,
	DOCUMENT_NODE: 9,
	DOCUMENT_TYPE_NODE: 10,
	DOCUMENT_FRAGMENT_NODE: 11,
	NOTATION_NODE: 12
}

function Codespace(container, options)
{
	var self = this;
	
	var selection = {
		// Normal position
		line:			0,
		pos:			0,
		// Selection position
		sline:			0,
		spos:			0
	};
	
	/*
	 *	Stores keyboard shortcuts
	 *	These should be of the form:
	 *		{ctrl: true, shift: true, alt: true, key: charcode, chr: '[uppercase character!]', callback: [text or handle]}
	 */
	var shortcuts = new Array();
	
	/*
	 *	Stores filter rules, for special actions on given keywords.
	 *	Example: spellcheck.
	 *		- name:				short name of the rule. This should match your css (1).
	 *		- description:		will be shown as a tooltip to the user.
	 *		- match:			callback to determine a match. return true if matches.
	 *		- onclick:			callback for when an item is clicked.
	 *		- onmouseover:
	 *		- onmouseout:
	 *		- oncursorin:		when the cursor enters an adjoining position.
	 *		- oncursorout:
	 *
	 *	(1) The css rule should be marked as .line div [rule="NAME"] {...}
	 */
	// TODO: change rules to be language-based instead!
	this.rules = [
		{name: 'spellcheck', description: 'This word is spelt "tast"!', match: function(a) {return a.match(/test/gi);}},
		{name: 'spellcheck', description: 'Yep, thats much better! ;-)', match: function(a) {return a.match(/tast/gi);}},
		{name: 'spellcheck', description: 'This is another test', match: function(a) {return a.match(/string/gi);}, oncursorin: function(a) {a.style.backgroundColor='blue';}, oncursorout: function(a) {a.style.backgroundColor='';}}
	];

	/*
	 *	And the same thing for events...
	 *	Current events are:
	 *		- click
	 *		- focus
	 *		- unfocus
	 */
	var events = {
		click: [],
		focus: [],
		unfocus: []
	};
	
	// The editor can only be written to when it is focused!
	this.focus = false;
	
	//var tokens = /\ \(\)\[\]\{\}\"\'\&\|\%\<\>\\\/\?\!\.\,\:\;\+\=\~\*\^\#/gi;
	//var tokens = " ()[]{}\"'&|%<>\/?!.,:;+=~*^#";
	//var tokens = /[\ \(\)\[\]\{\}\"\'\&\|\%\<\>\\\/\?\!\.\,\:\;\+\=\~\*\^\#]/gi;
	//var tokens = /([\(\)\[\]\{\}\"\'\&\|\%\<\>\\\/\?\!\.\,\:\;\+\=\~\*\^\#])/gi;
	
	// IMPORTANT! TABS MUST BE A TOKEN (due to extreme space differences!)
	var tokens = /(\t|\.)/gi;

	
	// Handle options
	var defaults = {
		language:	'default',
		readOnly:	false,
		text:		'\n'
	};
	if(!options) options = {};
	for(var i in defaults) if(!options[i]) options[i] = defaults[i];
	
	// lis have several properties:
	// text:		actual plain text it contains
	
	
	// And actually create the editor.
	// If the container is a textarea, do our little special thing
	if(typeof(container) == 'string') container = document.getElementById(container);
	if(container.tagName == 'TEXTAREA') {
		// TODO:	 implement!
		alert('Not implemented for textareas');
	}
	
	container.className = 'codespace';
	
	var editor = container.appendChild( document.createElement('OL') );
	editor.className = 'codespace editor';
	
	
	var static_reference = editor.appendChild( document.createElement('DIV') );
	static_reference.className = 'static';
	var caret_div = static_reference.appendChild( document.createElement('DIV') );
	caret_div.className = 'caret';
	
	
	function getLine(i) {
		if(i>=(editor.childNodes.length-1) || i<0) return null;
		return editor.childNodes[i+1];
	}
	function checkSelectionLineLimits() {
		if(selection.line<0) selection.line = 0;
		if(selection.line>=editor.childNodes.length-1) selection.line = editor.childNodes.length - 1 - 1;
		if(selection.pos<0) selection.pos = 0;
		if(selection.pos>getLine(selection.line).textValue.length) selection.pos = getLine(selection.line).textValue.length;
		
		if(selection.sline<0) selection.sline = 0;
		if(selection.sline>=editor.childNodes.length-1) selection.sline = editor.childNodes.length - 1 - 1;
		if(selection.spos<0) selection.spos = 0;
		if(selection.spos>getLine(selection.sline).textValue.length) selection.spos = getLine(selection.sline).textValue.length;
	}
	
	/*
	 * A handy function to get the position of an element...
	 */
	function getPosition(element) {
		var t = element, x = 0, y = 0;
		if(!t) return {x:0,y:0,w:0,h:0};
		do {x += t.offsetLeft; y += t.offsetTop;} while(t = t.offsetParent);
		return {x:x, y:y, w:element.offsetWidth, h:element.offsetHeight};
	}

	var correction = getPosition(caret_div);
	caret_div.skewx = correction.x;
	caret_div.skewy = correction.y;
	// TODO: handle DOM-wise copy/pasting...
	// editor.oncut.oncopy.onpaste = ??
	
	/*var reference_size = document.body.appendChild( document.createElement('LI') );
	reference_size.className = 'codespace editor line';
	reference_size.txt = 'ABCDEFGHIJKLMONPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"()[]<>\\+=/:.,?!';
	reference_size.innerHTML = '<DIV>'+reference_size.txt+'</DIV>';
	reference_size.style.width = '1px';
	reference_size.style.height = '1px';
	reference_size.style.whiteSpace = 'nowrap';
	alert(reference_size.scrollWidth/reference_size.txt.length);*/
	
	
	
	
	
	/*
	 *	This splits the string into several different bits, and marks the line as
	 *	dirty if something was changed, so that the SyntaxHighlighter can pass over it.
	 */
	function textTokenizer(li) {
		
		if(!li.firstChild) li.appendChild( document.createElement('div') );
		var a = li.firstChild.childNodes;
		var txt = li.textValue || '\n';
		txt = txt.replace(tokens, ' $1 ');
		var bits = txt.split(' ');
		var i;
		
		for(i = 0; i<2*bits.length-1; i++) {
			if(i%2) bits[Math.floor(i/2)] = ((bits[Math.floor((i-1)/2)].match(tokens) ? '' : (bits[Math.floor((i+1)/2)].match(tokens) ? '' : ' ')));
			// TODO: if problems appear, insert 'real' space elements around the spacer span...
			
			var b = (i<a.length) ? a[i] : li.firstChild.appendChild( document.createElement('span') );
			
			if(b && b.innerHTML != bits[Math.floor(i/2)]) {
				b.innerHTML = bits[Math.floor(i/2)];
				
				// Clear potential old attributes...
				//console.log(b)
				if(b.oncursorout) b.oncursorout(b);
				b.removeAttribute('title');
				b.removeAttribute('rule');
				b.onclick = 		null;
				b.onmouseover = 	null;
				b.onmouseout =		null;
				b.oncursorin =		null;
				b.oncursorout =		null;
				
				// And apply (new?) rules here...
				for(var r=0; r<self.rules.length; r++) {
					var c = self.rules[r];
					if(c.match(bits[Math.floor(i/2)])) {
						b.onclick = 		c.onclick;
						b.onmouseover =		c.onmouseover;
						b.onmouseout =		c.onmouseout;
						b.oncursorin =		c.oncursorin;
						b.oncursorout =		c.oncursorout;
						b.setAttribute('title', c.description);
						b.setAttribute('rule', c.name);
						break;
					}
				}
				
				li.dirty = true;
			}
		}
		
		while(i < a.length) li.firstChild.removeChild(a[i]);
	}
	
	/*
	 *	This is invoked when the user presses a key. Any key. Anywhere.
	 *	When it is able to handle the key, the event is blocked.
	 */
	var key_processed = false;
	function keyHandler(evt) {
		if(!self.focus || options.readOnly) return true;
		evt = evt || window.event;
		var chr;
		
		/*
		 *	The characters can now be correctly understood
		 *	Handle all "normal" keys during a keypress event
		 */
		if(evt.type == "keypress") {
			if(!key_processed) {
				if(!evt.which) chr = String.fromCharCode(evt.keyCode);  			// IE
				else if(evt.which > 0) chr = String.fromCharCode(evt.which);		// All others
				else return true;
				if(key) key(chr);
				if(evt.preventDefault) evt.preventDefault();						// Gecko and Others
				evt.returnValue = false;											// IE
				return false;
			}
			else {key_processed = false;return true;}
		}
		
		/*
		 *	This is called first, but with less information under most browsers...
		 *	Handle special keys and shortcuts here...
		 */
		else {
			var fromChar = String.fromCharCode( evt.charCode || evt.keyCode );
			key_processed = false;
			
			// Search for any shortcuts, and stop the flow if needed...
			for(var i=0; i<shortcuts.length; i++) {
				var a = shortcuts[i];
				if((!a.key || a.key == evt.keyCode) && (!a.chr || a.chr == fromChar) && (a.shift==null || a.shift == evt.shiftKey) &&
				(a.ctrl==null || a.ctrl == evt.ctrlKey || a.ctrl == evt.metaKey) && (a.alt==null || a.alt == evt.altKey)) {
					try {
						if(shortcut) key_processed = (shortcut(a.callback, {key: evt.keyCode, chr: fromChar, shift: evt.shiftKey, ctrl: evt.ctrlKey || evt.metaKey, alt: evt.altKey})) || key_processed;
						else key_processed = true;
					} catch(e) {
						if(window.console) console.error('Shortcut callback function error: '+e);
					}
				}
			}
			
			// Handle problematic "normal" characters here...
			if(!key_processed) {
				key_processed = true;
				switch(evt.keyCode) {
				case 9:		// TAB
					if(key) key('\t');
					break;
				default:
					key_processed = false;
					break;
				}
			}
			
			// And prevent the default action only if necessary
			if(key_processed) {
				if(evt.preventDefault) evt.preventDefault();						// Gecko and Others
				evt.returnValue = false;											// IE
				return false;
			}
			else if((evt.keyCode && evt.keyCode>=112 && evt.keyCode<=123) ||		// Avoid F1-F12 keys
			(evt.ctrlKey && !evt.altKey)											// CTRL key is pressed and not alt key
			) {
				key_processed = true;
				return false;
			}
		}
		return true;
	}
	
	
	function key(k) {
		self.insertText(k);
	}

	function shortcut(callback, c) {
		if(!callback) return true;
		if(typeof(callback) == 'string') {
			var a = function() {
				eval(callback);
			}
			a.call(c);
			return true;
		}
		return callback(c);
	}
	
	
	
	/*
	 *	This code segment handles everything to do with the selection, and position handling...
	 *	The getRange function was adapted from http://www.quirksmode.org/dom/range_intro.html.
	 */
	function getRange() {
		var userSelection;
		if (window.getSelection) userSelection = window.getSelection();
		
		// should come last; Opera!
		else if (document.selection) userSelection = document.selection.createRange();

		if(userSelection.getRangeAt) return userSelection.getRangeAt(0);
		else { // Safari!
			var range = document.createRange();
			range.setStart(userSelection.anchorNode,userSelection.anchorOffset);
			range.setEnd(userSelection.focusNode,userSelection.focusOffset);
			return range;
		}
	}
	
	/*
	 *	Detects changes in the cursor position based on user interaction
	 */
	function updateSelection() {
		var range = getRange();
		if(!range) return;
		
		try {
			var sli, soffset, sl=null;
			var eli, eoffset, el=null;
			
			// Start position
			var r1 = range.cloneRange();
			for(var o = r1.startContainer; o = o.parentNode;) if(o.tagName == 'LI') {sli = o;break;}
			if(!sli) {sli = editor.firstChild;range.setStart(sli, 0);}
			r1.collapse(true);
			r1.setStart(sli, 0);
			soffset = r1.toString().length;
			r1.detach();
			
			// End position
			var r2 = range.cloneRange();
			for(var o = r2.endContainer; o = o.parentNode;) if(o.tagName == 'LI') {eli = o;break;}
			if(!eli) {eli = editor.lastChild;range.setEnd(eli, 0);}
			r2.setStart(eli, 0);
			eoffset = r2.toString().length;
			r2.detach();
			
			//range.detach();
			for(var i=0, cnt=0; i<sli.parentNode.childNodes.length; i++) {
				var a = sli.parentNode.childNodes[i];
				if(a == sli) sl = cnt;
				if(a == eli) el = cnt;
				if(sl!=null && el!=null) break;
				if(a.tagName == sli.tagName) cnt++;
			}
			
			selection.line = sl;
			selection.pos = soffset;
			selection.sline = el;
			selection.spos = eoffset;
			selection.update();
			//return console.log('Cursor is: ['+soffset+' - '+eoffset+'] on lines ['+sl+' - '+el+'] - '+range.toString());
		} catch(e) {
			if(window.console) console.error(e);
			else alert(e);
			return;
		}
	}
	
	/*
	 *	Caret movement routines.
	 *	Moves the caret to display the position indicated by selection
	 */
	selection.update = function() {
		checkSelectionLineLimits();
		var range = document.createRange();
		
		var o = selection.pos;
		var line = getLine(selection.line);
		
		// Here we update our virtual "cursor".
		if(line != self.oldLine) {
			if(self.oldLine) self.oldLine.className = self.oldLine.className.replace(/\ ?active/gi, '');
			line.className += ' active';
			self.oldLine = line;
		}
		
		var word = line.firstChild.firstChild;
		while(word && o>=(word.innerHTML || word.nodeValue || '').length) {
			if(!word.nextSibling) break;
			o -= (word.innerHTML || word.nodeValue || '').length;
			word = word.nextSibling;
		}
		// And now update the user-visible selection!
		// Ranges are based on offset in characters only for TEXT nodes, thus we need to go one level deeper.
		if((selection.line == selection.sline && selection.pos<selection.spos) || (selection.line<selection.sline)) range.setStart(word.firstChild || word, o);
		else range.setEnd(word.firstChild || word, o);
		
		// And for the rest, we actually need a real element!
		while(word.nodeType == Node.TEXT_NODE) {word = word.nextSibling;o -= 1;}
		
		caret_div.style.left = (word.offsetLeft + Math.round((o/(word.innerHTML ? word.innerHTML.length : 1))*word.offsetWidth) - caret_div.skewx) + 'px';
		caret_div.style.height = word.offsetHeight + 'px';
		caret_div.style.top = (word.offsetTop - caret_div.skewy) + 'px';
		caret_div.blink = 0;
		caret_div.style.display = '';
		
		// Now send the "cursorin" and "cursorout" events if necessary.
		var o1 = word;
		var o2 = (o==0 ? word.previousSibling : (o==(word.firstChild ? word.firstChild.nodeValue.length : 0) ? word.nextSibling : null));
		if(o1 != self.o1) {
			if(self.o1 && self.o1.oncursorout && o2!=self.o1) self.o1.oncursorout(self.o1);
			if(o1 && o1.oncursorin) o1.oncursorin(o1);
			self.o1 = o1;
		}
		if(o2 != self.o2) {
			if(self.o2 && self.o2.oncursorout && o1!=self.o2) self.o2.oncursorout(self.o2);
			if(o2 && o2.oncursorin) o2.oncursorin(o2);
			self.o2 = o2;
		}
		
		// Now draw the visible user selection if there is one
		if(selection.line==selection.sline && selection.pos==selection.spos) range.collapse(true);
		else {
			o = selection.spos;
			word = getLine(selection.sline).firstChild.firstChild;
			while(word && o>=(word.innerHTML || word.nodeValue || '').length) {
				if(!word.nextSibling) break;
				o -= (word.innerHTML || word.nodeValue || '').length;
				word = word.nextSibling;
			}
			if((selection.line == selection.sline && selection.pos<selection.spos) || (selection.line<selection.sline)) range.setEnd(word.firstChild || word, o);
			else range.setStart(word.firstChild || word, o);
		}
		
		// TODO: Do all browsers support the following line?
		var sel = window.getSelection();
		sel.removeAllRanges();
		sel.addRange(range);
	}
	selection.ensureVisible = function() {
		// Ensure that the caret is visible through the scroll...
		if(editor.scrollTop > caret_div.offsetTop) editor.scrollTop = caret_div.offsetTop;
		if((editor.scrollTop+editor.offsetHeight) < (caret_div.offsetTop+caret_div.offsetHeight)) editor.scrollTop = caret_div.offsetTop - editor.offsetHeight + caret_div.offsetHeight + 5;
	}
	setInterval(function() {
		caret_div.style.display = (caret_div.blink <= 1) ? 'block' : 'none';
		caret_div.blink++;
		if(caret_div.blink >= 3) caret_div.blink = 1;
	}, 700);

	
	/*
	 *	This is invoked when the user clicks.
	 *	When the click is in the editor, it is selected.
	 */
	function mouseHandler(evt) {
		evt = evt || window.event;
		var target = evt.target || evt.srcElement;
		
		if(options.readOnly) return;
		
		var t = target;
		do {
			if(t == document.body) {changeFocus(false);break;}
			if(t == container) {changeFocus(true);break;}
		} while(t = t.parentNode);

		// Update selection!
		if(self.focus) updateSelection();
		
		// Cancel event
		if(self.focus && evt.which == 3) {
			if(evt.preventDefault) evt.preventDefault();						// Gecko and Others
			evt.returnValue = false;											// IE
			return false;
		}
	}
	
	function changeFocus(focus) {
		if(focus != self.focus) {
			self.focus = focus;
			// Fire events
			if(focus) editor.className += ' focused';
			else editor.className = editor.className.replace(/\ ?focused/gi, '');
		}
	}

	/*
	 *	Next handle the key-catching callbacks.
	 *	This is pretty important (for a text editor)
	 */
	if(document.attachEvent) document.attachEvent('onkeypress', keyHandler);
	else document.addEventListener('keypress', keyHandler, false);
	if(document.attachEvent) document.attachEvent('onkeydown', keyHandler);
	else document.addEventListener('keydown', keyHandler, false);
	
	// Add some callbacks for a sense of interactiveness
	/*if(document.attachEvent) document.attachEvent('onmousedown', mouseHandler);
	else document.addEventListener('mousedown', mouseHandler, false);*/
	if(document.attachEvent) document.attachEvent('oncontextmenu', mouseHandler);
	else document.addEventListener('contextmenu', mouseHandler, false);
	if(document.attachEvent) document.attachEvent('onmouseup', mouseHandler);
	else document.addEventListener('mouseup', mouseHandler, false);
	/*if(document.attachEvent) document.attachEvent('ondblclick', mouseHandler);
	else document.addEventListener('dblclick', mouseHandler, false);*/
	
	
	/******************************************************************************************************************************************************************************
	 *	PUBLIC API FUNCTIONS
	 ******************************************************************************************************************************************************************************/
	
	/*
	 *	This sets either the selection, or the cursor position (only two parameters specified).
	 *	If values are negative, they are interpreted as from the end of the string - 1.
	 */
	this.setSelection = function(cursor_line, cursor_pos, selection_to_line, selection_to_pos, silent) {
		if(selection_to_line == null) selection_to_line = cursor_line;
		if(selection_to_pos == null) selection_to_pos = cursor_pos;
		
		var l = getLine(cursor_line);
		if(cursor_pos<0 && l) cursor_pos = l.textValue.length + 1 - cursor_pos;
		l = getLine(selection_to_line);
		if(selection_to_pos<0 && l) selection_to_pos = l.textValue.length + 1 - selection_to_pos;
		
		selection.line = cursor_line;
		selection.pos = cursor_pos;
		selection.sline = selection_to_line;
		selection.spos = selection_to_pos;
		
		if(!silent) {
			selection.update();
			selection.ensureVisible();
		}
	}
	
	this.getSelection = function() {return selection;}
	
	/*
	 *	This inserts text at the current caret position, erasing anything selected.
	 */
	this.insertText = function(text, silent) {
		if(!text) text = '';
		text = text.replace(/\n?\r\n?/g, '\n');
		text = text.split('\n');
		
		if(selection.pos!=selection.spos || selection.line!=selection.sline) {
			// TODO: erase selection!
			// console.log("Some text needs to be removed here!");
			if(selection.line == selection.sline) {
				var line = getLine(selection.line);
				var a = line.textValue.slice(0, Math.min(selection.pos, selection.spos));
				var b = line.textValue.slice(Math.max(selection.pos, selection.spos), line.textValue.length);
				line.textValue = a.concat(b);
				textTokenizer(line);
				selection.pos = Math.min(selection.pos, selection.spos);
			}
			else {
				var line = getLine(Math.min(selection.line, selection.sline));
				var lineto = getLine(Math.max(selection.line, selection.sline));
				if(selection.line < selection.sline) line.textValue = line.textValue.slice(0, selection.pos) + lineto.textValue.slice(selection.spos, lineto.textValue.length);
				else line.textValue = line.textValue.slice(0, selection.spos) + lineto.textValue.slice(selection.pos, lineto.textValue.length);
				textTokenizer(line);
				for(var i=Math.max(selection.line, selection.sline); i>Math.min(selection.line, selection.sline); i--) {
					line = getLine(i);
					editor.removeChild(line);
				}
				selection.line = Math.min(selection.line, selection.sline);
				selection.pos = (selection.line < selection.sline) ? selection.pos : selection.spos;
			}
		}
		
		var last_offset_correction = 0;
		var oldline = null;
		for(var i=0; i<text.length; i++) {
			if(i>0) {
				selection.line++;
				selection.pos = 0;
			}

			var line = null;
			if(i==0) line = getLine(selection.line);
			if(!line) {
				line = (oldline && oldline.nextSibling) ? editor.insertBefore( document.createElement('LI'),  oldline.nextSibling) : editor.appendChild( document.createElement('LI') );
				line.textValue = '';
			}
			oldline = line;
			
			var a = line.textValue.slice(0, selection.pos);
			var b = line.textValue.slice(selection.pos, line.textValue.length);
			
			// TODO: add potential indentation to front of a for all i>0
			
			if(i==text.length-1) {
				line.textValue = (i==0) ? a.concat(text[i]).concat(b) : a.concat(text[i]);
			}
			else {
				line.textValue = a.concat(text[i]);
				text[text.length-1] = text[text.length-1].concat(b);
				last_offset_correction += b.length;
			}
			textTokenizer(line);
			selection.pos += text[i].length - last_offset_correction;
		}
		
		selection.sline = selection.line;
		selection.spos = selection.pos;
		
		if(!silent) {
			selection.update();
			selection.ensureVisible();
		}
	}
	
	
	/*
	 *	Shortcuts. This initiates a callback for certain key sequences.
	 *	The format is one or more of {ctrl: true, shift: true, alt: true, key: charcode, chr: '[uppercase character!]', callback: [text or handle]}
	 *	If a callback is not specified, the key press will simply be ignored!
	 *
	 *	Warning! Do not attempt to open modal dialogs (alert boxes, etc) during the callback, as this prevents the key from being effectively caught, and the
	 *	typed character will be inserted regardless of what you return.
	 */
	this.registerShortcut = function(shortcut) {
		shortcuts.push(shortcut);
	}
	this.unregisterShortcut = function(shortcut) {
		var where = shortcuts.indexOf(shortcut);
		if(where<0) return;
		shortcuts.splice(where, 1);
	}
	
	/*
	 *	This retrieves content from the editor
	 *	getSelectedText: 	returns all the text in the given selection.
	 *	getText:			returns all text
	 *	getWords:			returns an arraw of span elements forming the different words on a line, or null if the line is invalid.
	 */
	this.getWords = function(line) {
		var a = getLine(line == null? selection.line : line);
		if(!a) return null;
		
		var ret = new Array();
		for(var i=0; i<a.firstChild.childNodes.length; i++) {
			if(a.firstChild.childNodes[i].nodeType != Node.TEXT_NODE) ret.push(a.firstChild.childNodes[i]);
		}
		return ret;
	}
	
	
	/* **************************************************************
	 *	And initial loading operations!
	 */
	// DEBUG
	function tata() {
		for(var i = 0; i<100; i++) {
			var a = '';
			if(!(i%5)) a+='Lorem Ipsum is simply dummy text of the printing and typesetting industry.';
			else if(!((i-1)%5)) a+='Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.';
			else if(!((i-2)%5)) a+='It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.';
			else if(!((i-3)%5)) a+='It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.';
			self.insertText(a+'\n', true);
		}
		self.setSelection(0,0);
	}
	tata();

	
	self.registerShortcut({key: 39, callback: function(a) {			// RIGHT ARROW
		var b = getLine(selection.line);
		if(selection.pos+1 <= (b ? b.textValue.length : 0)) self.setSelection(selection.line, selection.pos+1, a.shift ? selection.sline : null, a.shift ? selection.spos : null);
		else self.setSelection(selection.line+1, 0, a.shift ? selection.sline : null, a.shift ? selection.spos : null);
		return true;
	}});

	self.registerShortcut({key: 37, callback: function(a) {			// LEFT ARROW
		var b = getLine(selection.line-1);
		if(selection.pos > 0) self.setSelection(selection.line, selection.pos-1, a.shift ? selection.sline : null, a.shift ? selection.spos : null);
		else self.setSelection(selection.line-1, b ? b.textValue.length : 0, a.shift ? selection.sline : null, a.shift ? selection.spos : null);
		return true;
	}});
	
	self.registerShortcut({key: 38, callback: function(a) {			// UP ARROW
		// TODO: cater for multi-line lines
		self.setSelection(selection.line-1, selection.pos, a.shift ? selection.sline : null, a.shift ? selection.spos : null);
		return true;
	}});
	
	self.registerShortcut({key: 40, callback: function(a) {			// DOWN ARROW
		// TODO: cater for multi-line lines
		var offset = selection.pos;
		var word = getLine(selection.line).firstChild.firstChild;
		while(word && offset>=(word.innerHTML || word.nodeValue || '').length) {
			if(!word.nextSibling) break;
			offset -= (word.innerHTML || word.nodeValue || '').length;
			word = word.nextSibling;
		}
		
		offset = selection.pos + (word ? ((word.innerHTML || word.nodeValue || '').length - offset) : 0);
		if(word) word = word.nextSibling;
		//if(word) offset -= (word.innerHTML || word.nodeValue || '').length;
	
		var c = (caret_div.offsetLeft + caret_div.skewx);
		while(word && (word = word.nextSibling)) {
			if(word.offsetLeft<=c && (word.offsetLeft+word.offsetWidth)>=c) {
				if(window.console) console.log(word);
				offset += Math.round(((c-word.offsetLeft)/word.offsetWidth)*((word.innerHTML || word.nodeValue || '').length) + 1);
				self.setSelection(selection.line, offset, a.shift ? selection.sline : null, a.shift ? selection.spos : null);
				return true;
			}
			offset += (word.innerHTML || word.nodeValue || '').length;
		}
		if(window.console) console.log('NOT FOUND');
		self.setSelection(selection.line+1, selection.pos, a.shift ? selection.sline : null, a.shift ? selection.spos : null);
		return true;
	}});
	
	self.registerShortcut({key: 33, callback: function(a) {			// PAGE UP
		editor.scrollTop -= editor.offsetHeight - 20;
		return true;
	}});
	
	self.registerShortcut({key: 34, callback: function(a) {			// PAGE DOWN
		editor.scrollTop += editor.offsetHeight - 20;
		return true;
	}});
	
	self.registerShortcut({key: 46, callback: function(a) {			// DELETE
		if(selection.pos!=selection.spos || selection.line!=selection.sline) {self.insertText('');return true;}
		var b = getLine(selection.line).textValue.length == selection.pos;
		self.setSelection(selection.line, selection.pos, b ? selection.sline+1 : selection.sline, b ? 0 : selection.spos+1);
		self.insertText('');
		return true;
	}});
	
	self.registerShortcut({key: 8, callback: function(a) {			// BACKSPACE
		if(selection.pos!=selection.spos || selection.line!=selection.sline) {self.insertText('');return true;}
		var b = getLine(selection.line-1);
		self.setSelection(selection.line, selection.pos, selection.pos ? selection.sline : selection.sline-1, selection.pos ? selection.spos-1 : (b ? b.textValue.length : 0));
		self.insertText('');
		return true;
	}});
	
	
	function highlight() {
		old = null;
		for(var i=0, l; l = getLine(i); i++) {
			if(l.dirty) {
				if(Codespace.SyntaxHighlighter) {
					try {
						if(Codespace.SyntaxHighlighter(l, old, self.getWords(i), options)) {
							var g = getLine(i+1);
							if(g) g.dirty = true;
						}
					} catch(e) {if(window.console) console.error('SyntaxHighlighter error: '+e);}
					l.dirty = false;
				}
				break;
			}
			old = l;
		}
		setTimeout(highlight, 50);
	}
	highlight();
	
	// Freee some memory!
	options.text = '';
}




